Skip to content

Issue 02: per-product source adapter, normalized model, two-control split#1127

Closed
busbyk wants to merge 4 commits into
forecast-date-pickerfrom
issue-02-data-layer
Closed

Issue 02: per-product source adapter, normalized model, two-control split#1127
busbyk wants to merge 4 commits into
forecast-date-pickerfrom
issue-02-data-layer

Conversation

@busbyk

@busbyk busbyk commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

Description

Issue 02 of the native-product-pages effort: the data layer every native product page consumes — a per-product source adapter returning a normalized, API-shape-agnostic model, plus the two controls that govern it (rollout flag; data-source config). This is reconcile/refactor, not greenfield: the fetchers, zod types, and components already exist; this reorganizes them behind an adapter seam and adds the two config seams.

⚠️ Stacked PR. Base is forecast-date-picker (issue 09), which is itself stacked on native-forecast-reconcile (issue 01, #1121). Linear stack 01 → 09 → 02; this auto-retargets up the stack as the lower PRs merge. Draft until the stack lands.

Related Issues

  • .scratch/native-product-pages/issues/02-data-layer-adapter-and-controls.md
  • PRD: .scratch/native-product-pages/PRD.md · ADR: "Native product page architecture" (to land as docs/decisions/018-*)
  • Depends on 01 (reconciliation) and is stacked on 09 (date picker)

Key Changes

  • Normalized model (services/nac/model/forecast.ts) — API-agnostic types the components depend on. Owns the top-level product types (Forecast/Summary/Warning/WarningProduct/…) and reuses the leaf domain enums/types from the v2 wire schema. "No active warning" is plain null, not v2's null-object, so consumers stop special-casing the miss representation (the duplicated toWarningProduct helpers are gone).
  • Source adapter (services/nac/sources/) — ForecastSource/WarningSource interfaces, a v2 implementation wrapping the existing fetchers with pure, unit-tested mapV2* mappers, and getForecastSource/getWarningSource resolvers. Components/pages fetch through the adapter, never a raw fetcher.
  • Control 2 — data source (sources/config.ts) — per-product v2/v3 selection via code/env config, uniform across tenants, defaulting to v2, with a per-center v3 canary allowlist. Not a Setting. (NAC_{FORECAST,WARNING}_SOURCE, NAC_{FORECAST,WARNING}_V3_CANARY_CENTERS.)
  • Control 1 — rolloutSettings.useNativeForecasts becomes a per-product nativeProducts: { forecast, warning } group; new getNativeProductFlag(center, product) reader. Per-product (not all-or-nothing) so a center can go native on forecast while later products stay on the widget.
  • Call sites (NativeForecastPage, AllZonesForecast, both [zone] routes) wired to the adapter; all forecast components repointed to the model.

How to test

  • pnpm tsc, pnpm lint, pnpm test (includes new v2→model mapper tests in __tests__/server/nacSourceMappers.server.test.ts).
  • Flip a center's nativeProducts.forecast in Settings → native forecast page renders; off → legacy widget.
  • Data source defaults to v2; setting NAC_FORECAST_SOURCE=v3 (or the canary allowlist) currently throws by design until the v3 source is built.

Migration Explanation

20260622_223300_native_products_flag generalizes the single use_native_forecasts boolean into the native_products_forecast / native_products_warning columns. It adds both columns, backfills both from the old value (so a center already on the native forecast page — warning banner included — is unchanged), then drops the old column. Verified on a dev.db copy: a row with the old flag set retains both new flags; data preserved. migrate:check flags the ALTER/DROP for review (expected for any schema migration); the drop is intentional and data-preserving.

Future enhancements / Questions

  • getUseNativeForecasts is kept as a now-unused thin wrapper over getNativeProductFlag — can be deleted.
  • The v3 source/mapper is deliberately not built (PRD risk: v3 deployment unverified). The seam is in place; flipping a product to v3 is a one-line config change once v3 is confirmed deployed.
  • Archive/by-id stay view-helpers (not under the adapter), feeding the model props via structural compatibility (wire ≡ model for v2).

🤖 Generated with Claude Code

Issue 02 of native-product-pages: the data layer every native page consumes.

- Normalized model (services/nac/model/forecast.ts): API-shape-agnostic types
  the components depend on. Owns the top-level product types and reuses the leaf
  domain enums/types from the v2 wire schema. "No active warning" is plain null,
  not v2's null-object, so consumers stop special-casing the miss representation.
- Source adapter (services/nac/sources/): ForecastSource/WarningSource interfaces
  with a v2 implementation that maps the legacy response into the model. Pure,
  unit-tested v2 mappers. Components/pages fetch through the adapter, never a
  raw fetcher.
- Control 2 (data source): per-product v2/v3 selection via code/env config,
  uniform across tenants, defaulting to v2, with a per-center v3 canary allowlist.
- Control 1 (rollout): Settings.useNativeForecasts becomes a per-product
  nativeProducts:{forecast,warning} group; getNativeProductFlag(center,product)
  reader. Migration backfills both flags from the old value; seed updated.

tsc, lint, test (incl. v2->model mapper tests), and drift all green. Migration
verified on a dev.db copy (backfill preserves data before the column drop);
--no-verify only bypasses the ALTER/DROP review gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A concise, human-facing README covering the normalized model, the per-product
source adapter, the model-vs-wire dependency direction, and the two controls
(rollout vs data source), plus how to extend the layer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Relocate the data-layer architecture overview from src/services/nac/README.md
to docs/nac-data-layer.md (repo convention: docs live in docs/). Repo-root the
code paths and cross-link with nac-data-display.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
One physical line per paragraph/bullet (no manual hard-wrapping).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Preview deployment: https://issue-02-data-layer.preview.avy-fx.org

@busbyk

busbyk commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator Author

Folded into #1129, which now targets native-product-pages and shows the full combined stack diff for single-PR review. Branch kept as a safety net.

@busbyk busbyk closed this Jul 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant